/*
 * NumberRange.java
 *
 * Created on July 28, 2001, 12:39 AM
 *
 * Copyright 2008 the Desert Labs Project (http://desertlabs.us)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package us.desertlabs.number;

import java.math.BigDecimal;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class is used to represent a numeric range, which is a set of numbers
 * with a minimum and maximum value, inclusive. It maintains a randomizer that
 * can be used to supply instances of numeric objects that fall within the
 * range, as well as a method for retrieving the numeric value that lies at a
 * specific point within the range, and a method for determining whether or not
 * a given value falls within the range.
 *
 * @author Brandon Franklin
 */
public class NumberRange implements Cloneable {

    /**
     * Checks a proposed minimum and maximum pair for validity, and returns a
     * boolean result describing that validity.
     *
     * @param minimum a proposed minimum value for the range
     * @param maximum a proposed maximum value for the range
     * @return true if the proposed values are acceptable, false if they are not
     */
    private static boolean checkMinAndMax( final Number minimum, final Number maximum ) {
        final BigDecimal minDec = new BigDecimal( minimum.toString() );
        final BigDecimal maxDec = new BigDecimal( maximum.toString() );
        return (maxDec.compareTo( minDec ) >= 0);
    }

    /** The maximum possible value of this range */
    private Number maximum;

    /** The minimum possible value of this range */
    private Number minimum;

    /**
     * Creates a new <code>NumberRange</code>. The minimum and maximum values
     * will be set to the default values of 0 and 100, respectively.
     */
    public NumberRange() {
        this( 0, 100 );
    }

    /**
     * Creates a new <code>NumberRange</code> with the specified minimum and
     * maximum values.
     *
     * @param minimum the minimum possible value of the range
     * @param maximum the maximum possible value of the range
     * @throws IllegalArgumentException if the supplied maximum value is lower
     *         than the supplied minimum value
     */
    public NumberRange( final Number minimum, final Number maximum ) {

        // Perform a very generic comparison of the two values
        if( !checkMinAndMax( minimum, maximum ) ) {
            throw new IllegalArgumentException( "Maximum value cannot be less than minimum value." );
        }

        this.maximum = maximum;
        this.minimum = minimum;
    }

    /**
     * Builds and returns a clone of this object.
     *
     * @return a clone of this object
     */
    @Override
    public Object clone() {
        NumberRange clone = null;
        try {
            clone = (NumberRange)super.clone();
        } catch( final CloneNotSupportedException cnse ) {
            Logger.getLogger( getClass().getName() ).log( Level.SEVERE, "Unable to clone NumberRange object.", cnse );
        }

        return clone;
    }

    /**
     * Compares this <CODE>NumberRange</CODE> with the specified <CODE>Object</CODE>
     * for equality.
     *
     * @param obj <CODE>Object</CODE> to which this <CODE>NumberRange</CODE>
     *        is to be compared
     * @return <CODE>true</CODE> if and only if the specified <CODE>Object</CODE>
     *         is a <CODE>NumberRange</CODE> whose minimum and maximum values
     *         are numerically equal to those of this <CODE>NumberRange</CODE>
     */
    @Override
    public boolean equals( final Object obj ) {
        if( obj == this ) {
            return true;

        } else if( obj instanceof NumberRange ) {
            final NumberRange otherRange = (NumberRange)obj;

            return (minimum.equals( otherRange.minimum ) && maximum.equals( otherRange.maximum ));

        } else {
            return false;
        }
    }

    /**
     * Returns the maximum possible value of this range.
     *
     * @return the maximum possible value of this range
     */
    public Number getMaximum() {
        return maximum;
    }

    /**
     * Returns the minimum possible value of this range.
     *
     * @return the minimum possible value of this range
     */
    public Number getMinimum() {
        return minimum;
    }

    /**
     * Builds and returns a new <code>Number</code> object that falls within
     * this range. The exact value of the new object will be generated randomly.
     *
     * @return a new random <code>Number</code> object that falls within this
     *         range
     */
    public Number getRandomNumber() {
        return getValueAt( Math.random() );
    }

    /**
     * Returns a new <code>Number</code> object that falls at a specific point
     * within the range. This number is calculated by multiplying the breadth of
     * the range by the supplied parameter, then returning the value at the
     * resulting offset within the range. This means that supplying a parameter
     * of 0.1 will return the value that lies one-tenth of the way through the
     * range, while supplying a 0.5 will supply the value lying exactly in the
     * range's center. Supplying other values is allowed, but will return
     * numbers that fall outside the bounds of the range.
     *
     * @param location the location in the range, generally a value from 0.0 to
     *        1.0, that the returned value will be from
     * @return the <code>Number</code> lying at the specified location within
     *         the range
     */
    public Number getValueAt( final double location ) {
        final BigDecimal minDec = new BigDecimal( minimum.toString() );
        final BigDecimal maxDec = new BigDecimal( maximum.toString() );
        BigDecimal locDec = new BigDecimal( location );

        locDec = locDec.multiply( maxDec.subtract( minDec ) );
        locDec = locDec.add( minDec );

        return locDec;
    }

    /**
     * Returns the hash value for this <CODE>NumberRange</CODE>, which will
     * be generated based on its minimum and maximum values, using the algorithm
     * supplied by the value <CODE>Number</CODE> objects' implementations.
     *
     * @return the hash value for this <CODE>NumberRange</CODE>
     */
    @Override
    public int hashCode() {
        return (minimum.hashCode() + maximum.hashCode());
    }

    /**
     * Returns true if the supplied <code>Number</code> is within this range,
     * false otherwise.
     *
     * @param value the <code>Number</code> to be checked
     * @return true if the supplied <code>Number</code> is within this range,
     *         false otherwise
     */
    public boolean includes( final Number value ) {
        final BigDecimal minDec = new BigDecimal( minimum.toString() );
        final BigDecimal maxDec = new BigDecimal( maximum.toString() );
        final BigDecimal valDec = new BigDecimal( value.toString() );

        if( (valDec.compareTo( minDec ) < 0) || (valDec.compareTo( maxDec ) > 0) ) {
            return false;
        }
        return true;
    }

    /**
     * Sets the maximum possible value of this range.
     *
     * @param maximum the maximum possible value of this range
     * @throws IllegalArgumentException if the supplied maximum value is lower
     *         than the current minimum value
     */
    public void setMaximum( final Number maximum ) {

        // Perform a very generic check of the proposed maximum value
        if( !checkMinAndMax( minimum, maximum ) ) {
            throw new IllegalArgumentException( "Maximum value cannot be less than minimum value." );
        }

        this.maximum = maximum;
    }

    /**
     * Sets the minimum possible value of this range.
     *
     * @param minimum the minimum possible value of this range
     * @throws IllegalArgumentException if the supplied minimum value is higher
     *         than the current maximum value
     */
    public void setMinimum( final Number minimum ) {

        // Perform a very generic check of the proposed minimum value
        if( !checkMinAndMax( minimum, maximum ) ) {
            throw new IllegalArgumentException( "Maximum value cannot be less than minimum value." );
        }

        this.minimum = minimum;
    }

    /**
     * Returns a <CODE>String</CODE> representation of this range.
     *
     * @return a <CODE>String</CODE> representation of this range
     */
    @Override
    public String toString() {

        final StringBuilder sb = new StringBuilder();
        sb.append( "{ minimum:" );
        sb.append( minimum );
        sb.append( ", maximum:" );
        sb.append( maximum );
        sb.append( " }" );

        return sb.toString();
    }
}
